package org.stjs.generator.javac;

/*>>>
 import checkers.nullness.quals.Nullable;
 */

import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Deque;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import javax.lang.model.element.Element;
import javax.lang.model.element.ElementKind;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.Modifier;
import javax.lang.model.element.Name;
import javax.lang.model.element.PackageElement;
import javax.lang.model.element.TypeElement;
import javax.lang.model.element.VariableElement;
import javax.lang.model.type.DeclaredType;
import javax.lang.model.type.TypeKind;
import javax.lang.model.type.TypeMirror;
import javax.lang.model.util.ElementFilter;
import javax.lang.model.util.Elements;

import com.sun.tools.javac.code.Symbol;

/**
 * A Utility class for analyzing {@code Element}s.
 */
@SuppressWarnings("PMD")
public final class ElementUtils {

	// Class cannot be instantiated.
	private ElementUtils() {
		throw new AssertionError("Class ElementUtils cannot be instantiated.");
	}

	/**
	 * Returns the innermost type element enclosing the given element
	 * 
	 * @param elem
	 *            the enclosed element of a class
	 * @return the innermost type element
	 */
	public static TypeElement enclosingClass(final Element elem) {
		Element result = elem;
		while (result != null && !result.getKind().isClass()
				&& !result.getKind().isInterface()) {
			/* @Nullable */Element encl = result.getEnclosingElement();
			result = encl;
		}
		return (TypeElement) result;
	}

	/**
	 * Returns the innermost package element enclosing the given element. The
	 * same effect as
	 * {@link javax.lang.model.util.Elements#getPackageOf(Element)}. Returns the
	 * element itself if it is a package.
	 * 
	 * @param elem
	 *            the enclosed element of a package
	 * @return the innermost package element
	 * 
	 */
	public static PackageElement enclosingPackage(final Element elem) {
		Element result = elem;
		while (result != null && result.getKind() != ElementKind.PACKAGE) {
			/* @Nullable */Element encl = result.getEnclosingElement();
			result = encl;
		}
		return (PackageElement) result;
	}

	/**
	 * Returns the "parent" package element for the given package element. For
	 * package "A.B" it gives "A". For package "A" it gives the default package.
	 * For the default package it returns null;
	 * 
	 * Note that packages are not enclosed within each other, we have to
	 * manually climb the namespaces. Calling "enclosingPackage" on a package
	 * element returns the package element itself again.
	 * 
	 * @param elem
	 *            the package to start from
	 * @return the parent package element
	 * 
	 */
	public static PackageElement parentPackage(final Elements e,
			final PackageElement elem) {
		String fqnstart = elem.getQualifiedName().toString();
		String fqn = fqnstart;
		if (!fqn.isEmpty() && fqn.contains(".")) {
			fqn = fqn.substring(0, fqn.lastIndexOf('.'));
			return e.getPackageElement(fqn);
		}
		return null;
	}

	/**
	 * Returns true if the element is a static element: whether it is a static
	 * field, static method, or static class
	 * 
	 * @param element
	 * @return true if element is static
	 */
	public static boolean isStatic(Element element) {
		return element.getModifiers().contains(Modifier.STATIC);
	}

	/**
	 * Returns true if the element is a final element: a final field, final
	 * method, or final class
	 * 
	 * @param element
	 * @return true if the element is final
	 */
	public static boolean isFinal(Element element) {
		return element.getModifiers().contains(Modifier.FINAL);
	}

	/**
	 * Returns true if the element is a effectively final element.
	 * 
	 * @param element
	 * @return true if the element is effectively final
	 */
	// public static boolean isEffectivelyFinal(Element element) {
	// Symbol sym = (Symbol) element;
	// if (sym.getEnclosingElement().getKind() == ElementKind.METHOD &&
	// (sym.getEnclosingElement().flags() & ABSTRACT) != 0) {
	// return true;
	// }
	// return (sym.flags() & (FINAL | EFFECTIVELY_FINAL)) != 0;
	// }

	/**
	 * Returns the {@code TypeMirror} for usage of Element as a value. It
	 * returns the return type of a method element, the class type of a
	 * constructor, or simply the type mirror of the element itself.
	 * 
	 * @param element
	 * @return the type for the element used as a value
	 */
	public static TypeMirror getType(Element element) {
		if (element.getKind() == ElementKind.METHOD) {
			return ((ExecutableElement) element).getReturnType();
		} else if (element.getKind() == ElementKind.CONSTRUCTOR) {
			return enclosingClass(element).asType();
		} else {
			return element.asType();
		}
	}

	/**
	 * Returns the qualified name of the inner most class enclosing the provided
	 * {@code Element}
	 * 
	 * @param element
	 *            an element enclosed by a class, or a {@code TypeElement}
	 * @return The qualified {@code Name} of the innermost class enclosing the
	 *         element
	 */
	public static/* @Nullable */Name getQualifiedClassName(Element element) {
		if (element.getKind() == ElementKind.PACKAGE) {
			PackageElement elem = (PackageElement) element;
			return elem.getQualifiedName();
		}

		TypeElement elem = enclosingClass(element);
		if (elem == null) {
			return null;
		}

		return elem.getQualifiedName();
	}

	/**
	 * Returns a verbose name that identifies the element.
	 */
	public static String getVerboseName(Element elt) {
		if (elt.getKind() == ElementKind.PACKAGE || elt.getKind().isClass()) {
			return getQualifiedClassName(elt).toString();
		} else {
			return getQualifiedClassName(elt) + "." + elt.toString();
		}
	}

	/**
	 * Check if the element is an element for 'java.lang.Object'
	 * 
	 * @param element
	 *            the type element
	 * @return true iff the element is java.lang.Object element
	 */
	public static boolean isObject(TypeElement element) {
		return element.getQualifiedName().contentEquals("java.lang.Object");
	}

	/**
	 * Returns true if the element is a constant time reference
	 */
	public static boolean isCompileTimeConstant(Element elt) {
		return elt != null && elt.getKind() == ElementKind.FIELD
				&& ((VariableElement) elt).getConstantValue() != null;
	}

	/**
	 * Returns true if the element is declared in ByteCode. Always return false
	 * if elt is a package.
	 */
	public static boolean isElementFromByteCode(Element elt) {
		if (elt == null) {
			return false;
		}

		if (elt instanceof Symbol.ClassSymbol) {
			Symbol.ClassSymbol clss = (Symbol.ClassSymbol) elt;
			if (null != clss.classfile) {
				// The class file could be a .java file
				return clss.classfile.getName().endsWith(".class");
			} else {
				return false;
			}
		}
		return isElementFromByteCode(elt.getEnclosingElement(), elt);
	}

	/**
	 * Returns true if the element is declared in ByteCode. Always return false
	 * if elt is a package.
	 */
	private static boolean isElementFromByteCode(Element elt, Element orig) {
		if (elt == null) {
			return false;
		}
		if (elt instanceof Symbol.ClassSymbol) {
			Symbol.ClassSymbol clss = (Symbol.ClassSymbol) elt;
			if (null != clss.classfile) {
				// The class file could be a .java file
				return clss.classfile.getName().endsWith(".class")
						|| clss.classfile.getName().endsWith(".class)")
						|| clss.classfile.getName().endsWith(".class)]");
			} else {
				return false;
			}
		}
		return isElementFromByteCode(elt.getEnclosingElement(), elt);
	}

	/**
	 * Returns the field of the class
	 */
	public static VariableElement findFieldInType(TypeElement type, String name) {
		for (VariableElement field : ElementFilter.fieldsIn(type
				.getEnclosedElements())) {
			if (field.getSimpleName().toString().equals(name)) {
				return field;
			}
		}
		return null;
	}

	public static Set<VariableElement> findFieldsInType(TypeElement type,
			Collection<String> names) {
		Set<VariableElement> results = new HashSet<VariableElement>();
		for (VariableElement field : ElementFilter.fieldsIn(type
				.getEnclosedElements())) {
			if (names.contains(field.getSimpleName().toString())) {
				results.add(field);
			}
		}
		return results;
	}

	public static boolean isError(Element element) {
		return "com.sun.tools.javac.comp.Resolve$SymbolNotFoundError"
				.equals(element.getClass().getName());
	}

	/**
	 * Does the given element need a receiver for accesses? For example, an
	 * access to a local variable does not require a receiver.
	 * 
	 * @param element
	 *            The element to test.
	 * @return whether the element requires a receiver for accesses.
	 */
	public static boolean hasReceiver(Element element) {
		return element.getKind() != ElementKind.LOCAL_VARIABLE
				&& element.getKind() != ElementKind.PARAMETER
				&& element.getKind() != ElementKind.PACKAGE
				&& !ElementUtils.isStatic(element);
	}

	/**
	 * Determine all type elements for the classes and interfaces referenced in
	 * the extends/implements clauses of the given type element. TODO: can we
	 * learn from the implementation of
	 * com.sun.tools.javac.model.JavacElements.getAllMembers(TypeElement)?
	 */
	public static List<TypeElement> getSuperTypes(TypeElement type) {
		return getSuperTypes(type, true);
	}

	public static List<TypeElement> getSuperTypes(TypeElement type,
			boolean addInterfaces) {

		List<TypeElement> superelems = new ArrayList<TypeElement>();
		if (type == null) {
			return superelems;
		}

		// Set up a stack containing type, which is our starting point.
		Deque<TypeElement> stack = new ArrayDeque<TypeElement>();
		stack.push(type);

		while (!stack.isEmpty()) {
			TypeElement current = stack.pop();

			// For each direct supertype of the current type element, if it
			// hasn't already been visited, push it onto the stack and
			// add it to our superelems set.
			TypeMirror supertypecls = current.getSuperclass();
			if (supertypecls.getKind() != TypeKind.NONE) {
				TypeElement supercls = (TypeElement) ((DeclaredType) supertypecls)
						.asElement();
				if (!superelems.contains(supercls)) {
					stack.push(supercls);
					superelems.add(supercls);
				}
			}
			if (addInterfaces) {
				for (TypeMirror supertypeitf : current.getInterfaces()) {
					TypeElement superitf = (TypeElement) ((DeclaredType) supertypeitf)
							.asElement();
					if (!superelems.contains(superitf)) {
						stack.push(superitf);
						superelems.add(superitf);
					}
				}
			}
		}

		return Collections.<TypeElement> unmodifiableList(superelems);
	}

	/**
	 * Return all fields declared in the given type or any superclass/interface.
	 * TODO: should this use
	 * javax.lang.model.util.Elements.getAllMembers(TypeElement) instead of our
	 * own getSuperTypes?
	 */
	public static List<VariableElement> getAllFieldsIn(TypeElement type) {
		List<VariableElement> fields = new ArrayList<VariableElement>();
		fields.addAll(ElementFilter.fieldsIn(type.getEnclosedElements()));
		List<TypeElement> alltypes = getSuperTypes(type);
		for (TypeElement atype : alltypes) {
			fields.addAll(ElementFilter.fieldsIn(atype.getEnclosedElements()));
		}
		return Collections.<VariableElement> unmodifiableList(fields);
	}

	/**
	 * Return all methods declared in the given type or any
	 * superclass/interface. Note that no constructors will be returned. TODO:
	 * should this use javax.lang.model.util.Elements.getAllMembers(TypeElement)
	 * instead of our own getSuperTypes?
	 */
	public static List<ExecutableElement> getAllMethodsIn(TypeElement type) {
		return getAllMethodsIn(type, true);
	}

	public static List<ExecutableElement> getAllMethodsIn(TypeElement type,
			boolean addInterfaces) {
		List<ExecutableElement> meths = new ArrayList<ExecutableElement>();
		meths.addAll(ElementFilter.methodsIn(type.getEnclosedElements()));

		List<TypeElement> alltypes = getSuperTypes(type, addInterfaces);
		for (TypeElement atype : alltypes) {
			meths.addAll(ElementFilter.methodsIn(atype.getEnclosedElements()));
		}
		return Collections.<ExecutableElement> unmodifiableList(meths);
	}

	public static boolean sameSignature(ExecutableElement m1,
			ExecutableElement m2) {
		if (!m1.getSimpleName().equals(m2.getSimpleName())) {
			return false;
		}
		if (m1.getParameters().size() != m2.getParameters().size()) {
			return false;
		}
		for (int i = 0; i < m1.getParameters().size(); ++i) {
			if (!m1.getParameters().get(i).asType()
					.equals(m2.getParameters().get(i).asType())) {
				return false;
			}
		}
		return true;
	}

	/**
	 * 
	 * @param model
	 * @return the methods from the parent classes having the same signature as
	 *         the given method. it's useful when llong for annotations.
	 */
	public static List<ExecutableElement> getSameMethodFromParents(
			ExecutableElement model) {
		List<ExecutableElement> allMethods = ElementUtils.getAllMethodsIn(
				ElementUtils.enclosingClass(model), false);
		List<ExecutableElement> similar = new ArrayList<ExecutableElement>();
		for (ExecutableElement method : allMethods) {
			if (sameSignature(model, method)) {
				similar.add(method);
			}
		}
		return similar;
	}
}
